package org.iamd.conf;

import static javax.xml.parsers.DocumentBuilderFactory.newInstance;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import com.sun.org.apache.xml.internal.serialize.OutputFormat;
import com.sun.org.apache.xml.internal.serialize.XMLSerializer;

/**
 * Този клас се грижи за четенето, модификацията и файловите операции на дадена
 * конфигурация. Този клас е вътрешен за пакета.
 * 
 * @author И. Атанасов
 */
class Configuration {

	/**
	 * Енумерация използваща се за зареждане на системната конфигурация.
	 * 
	 * @author И. Атанасов
	 */
	enum ConfigurationTags {

		/** Таг за име */
		DESCRIPTION("name"),

		/** Таг за име */
		NAME("name"),

		/** Централен таг */
		PROPERTIES("properties"),

		/** Таг за entry */
		PROPERTY("property"),

		/** Таг за стойност */
		VALUE("value");

		/** Име на таг. */
		private String tagName;

		/**
		 * Инициализация на полето.
		 * 
		 * @param tagName
		 *            име на таг
		 */
		private ConfigurationTags(String tagName) {
			this.tagName = tagName;
		}

	}

	/** Конфигурационен файл. */
	private File configFile;

	/** DOM обект със съдържанието на конфигурацията. */
	private Document doc;

	/** Флаг показващ дали конфигурацията е подлежаща на модификация. */
	private boolean isEditable;

	/**
	 * Залагане на полетата, зареждане на конфигурацията в DOM обект.
	 * 
	 * @param configFile
	 *            конфигурационен файл
	 * @param isEditable
	 *            показва дали конфигурацията е модифицируема
	 * 
	 * @throws InitializationException
	 *             при неуспешно изчитане на конфигурационния файл
	 */
	Configuration(File configFile, boolean isEditable) throws InitializationException {
		try {
			this.configFile = configFile;
			this.isEditable = isEditable;
			this.doc = formDOM(this.configFile);
		} catch (Exception e) {
			throw new InitializationException(e);
		}
	}

	/**
	 * Експорт на конфигурацията във външен файл.
	 * 
	 * @param targetFile
	 *            Файла в който ще се експортира
	 * 
	 * @throws IOException
	 *             ако сериализацията е неуспешна
	 */
	void exportConfig(File targetFile) throws IOException {
		serializeConfiguration(targetFile);
	}

	/**
	 * По даден ключ връща намерена стойност. Ако не намери нищо - връща
	 * подадения входен параметър <code>defaultVal</code> като резултат
	 * (стойност по подразбиране).
	 * 
	 * @param key
	 *            ключ по който се търси
	 * @param defaultVal
	 *            стойност по подразбиране
	 * 
	 * @return намерената стойност
	 */
	String getProperty(IConfigProps key, String defaultVal) {
		return getAttribute(key, defaultVal, ConfigurationTags.VALUE);
	}

	/**
	 * По даден ключ връща намерено описание на даденото entry. Ако не намери
	 * нищо - връща подадения входен параметър <code>defaultVal</code> като
	 * резултат (стойност по подразбиране).
	 * 
	 * @param key
	 *            ключ по който се търси
	 * @param defaultVal
	 *            стойност по подразбиране
	 * 
	 * @return намереното описание
	 */
	String getPropertyDescription(IConfigProps key, String defaultVal) {
		return getAttribute(key, defaultVal, ConfigurationTags.DESCRIPTION);
	}

	/**
	 * Импортира конфигурационен файл във наличния зареден, след което съхранява
	 * променения наличен. Импорта става по следната логика - значими са само
	 * entry-та, които са налични и в двете конфигурации. За тези entry-та се
	 * вземат стойностите от външната конфигурация и се подменят във вътрешната
	 * налична конфигурация.
	 * 
	 * @param sourceFile
	 *            конфигурационния файл за импорт
	 * @throws ParserConfigurationException
	 *             при неуспешно изчитане на външната конфигурация
	 * @throws SAXException
	 *             при неуспешно изчитане на външната конфигурация
	 * @throws IOException
	 *             при неуспешно изчитане на външната или запис на вътрешната
	 *             конфигурация
	 */
	void importConfig(File sourceFile) throws ParserConfigurationException, SAXException,
			IOException {
		if (this.isEditable) {
			Document docSource = formDOM(sourceFile);
			NodeList nodesSource = docSource
					.getElementsByTagName(ConfigurationTags.PROPERTY.tagName);
			NodeList nodesTarget = this.doc
					.getElementsByTagName(ConfigurationTags.PROPERTY.tagName);
			for (int i = 0; i < nodesSource.getLength(); i++) {
				Element elemSource = (Element) nodesSource.item(i);
				String name = elemSource.getAttribute(ConfigurationTags.NAME.tagName);
				for (int j = 0; j < nodesTarget.getLength(); j++) {
					Element elemTarget = (Element) nodesTarget.item(j);
					if (name.equals(elemTarget.getAttribute(ConfigurationTags.NAME.tagName))) {
						elemTarget.setAttribute(ConfigurationTags.VALUE.toString(),
								elemSource.getAttribute(ConfigurationTags.VALUE.tagName));
						break;
					}
				}
			}
			saveConfig();
		}
	}

	/**
	 * Съхранява конфигурацията.
	 * 
	 * @throws IOException
	 *             ако сериализацията е неуспешна
	 */
	void saveConfig() throws IOException {
		serializeConfiguration(this.configFile);
	}

	/**
	 * Ако конфигурацията е редактируема, за намереното entry по подадения ключ
	 * се залага подадената стойност. Метода не извършва нищо ако конфигурацията
	 * не може да се редактира или няма намерено entry.
	 * 
	 * @param key
	 *            ключ по който се търси
	 * @param value
	 *            стойност за залагане
	 */
	void setProperty(String key, String value) {
		if (this.isEditable) {
			NodeList nodes = this.doc.getElementsByTagName(ConfigurationTags.PROPERTY.tagName);
			for (int i = 0; i < nodes.getLength(); i++) {
				Element elem = (Element) nodes.item(i);
				if (key.equals(elem.getAttribute(ConfigurationTags.NAME.tagName))) {
					elem.setAttribute(ConfigurationTags.VALUE.tagName, value);
					break;
				}
			}
		}
	}

	/**
	 * По подаден файл връща DOM обект.
	 * 
	 * @param sourceFile
	 *            конфигурационния файл за импорт
	 * 
	 * @return формирания обект
	 * 
	 * @throws ParserConfigurationException
	 *             при неуспешно изчитане на файла
	 * @throws SAXException
	 *             при неуспешно изчитане на файла
	 * @throws IOException
	 *             при неуспешно изчитане на файла
	 */
	private Document formDOM(File sourceFile) throws ParserConfigurationException, SAXException,
			IOException {
		DocumentBuilderFactory dbf = newInstance();
		DocumentBuilder docBuilder = dbf.newDocumentBuilder();
		return docBuilder.parse(sourceFile);
	}

	/**
	 * По даден ключ връща намерения атрибут на даденото entry. Ако не намери
	 * нищо - връща подадения входен параметър <code>defaultVal</code> като
	 * резултат (стойност по подразбиране).
	 * 
	 * @param key
	 *            ключ по който се търси
	 * @param defaultVal
	 *            стойност по подразбиране
	 * @param tag
	 *            типа на атрибута
	 * 
	 * @return намерената стойност на атрибута
	 */
	private String getAttribute(IConfigProps key, String defaultVal, ConfigurationTags tag) {
		NodeList nodes = this.doc.getElementsByTagName(ConfigurationTags.PROPERTY.tagName);
		for (int i = 0; i < nodes.getLength(); i++) {
			Element elem = (Element) nodes.item(i);
			if (key.getPropName().equals(elem.getAttribute(ConfigurationTags.NAME.tagName))) {
				return elem.getAttribute(tag.tagName);
			}
		}
		return defaultVal;
	}

	/**
	 * Сериализира текущата конфигурация в подадения файл.
	 * 
	 * @param targetFile
	 *            файла в който да се сериализира конфигурацията
	 * 
	 * @throws IOException
	 *             при неуспешна сериализация
	 */
	private void serializeConfiguration(File targetFile) throws IOException {
		Writer writer = new FileWriter(targetFile);
		OutputFormat format = new OutputFormat();
		format.setIndenting(true);
		XMLSerializer serializer = new XMLSerializer(writer, format);
		serializer.serialize(this.doc);
	}

}